/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.jfoenix.skins;

import java.util.ArrayList;
import java.util.List;

import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ParallelTransition;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.animation.Animation.Status;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Path;
import javafx.util.Duration;

import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;


/**
 * @author Shadi Shaheen & Bassel Mabsout
 * this UI allows the user to pick a color using HSL color system
 * 
 */
class JFXColorPickerUI extends Pane  {

	private CachedTransition selectorTransition;
	private int pickerSize = 400;
	// sl circle selector size
	private int selectorSize = 20;
	private double centerX,centerY, pickerRadius, huesRadius, huesSmallR, huesLargeR, slRadius;
	double currentHue = 0;
	
	private ImageView huesCircleView;
	private ImageView slCircleView;
	private Pane colorSelector;
	private Pane selector;
	private CurveTransition colorsTransition;
	
	public JFXColorPickerUI(int pickerSize){

		JFXDepthManager.setDepth(this, 1);
		
		this.pickerSize = pickerSize;	
		this.centerX = pickerSize/2;
		this.centerY = pickerSize/2;
		this.pickerRadius = pickerSize/2;
		this.huesRadius = pickerRadius * 0.9;
		this.huesSmallR = pickerRadius * 0.8;
		this.huesLargeR = pickerRadius;
		this.slRadius = pickerRadius * 0.7;
		
		// Create Hues Circle
		huesCircleView = new ImageView(getHuesCircle(pickerSize,pickerSize,new ArrayList<Stop>()));
		// clip to smooth the edges
		Circle outterCircle = new Circle(centerX, centerY, huesLargeR - 2);
		Circle innterCircle = new Circle(centerX, centerY, huesSmallR + 2);
		huesCircleView.setClip(Path.subtract(outterCircle, innterCircle));
		this.getChildren().add(huesCircleView);

		// create Hues Circle Selector
		Circle r1 = new Circle(pickerRadius - huesSmallR);		
		Circle r2 = new Circle(pickerRadius - huesRadius);
		colorSelector = new Pane();				
		colorSelector.setStyle("-fx-border-color:#424242; -fx-border-width:1px; -fx-background-color:rgba(255, 255, 255, 0.87);");
		colorSelector.setPrefSize(pickerRadius - huesSmallR,pickerRadius - huesSmallR);
		colorSelector.setShape(Path.subtract(r1, r2));
		colorSelector.setCache(true);
//		JFXDepthManager.setDepth(colorSelector, 1);
		colorSelector.setMouseTransparent(true);
		colorSelector.setPickOnBounds(false);
		this.getChildren().add(colorSelector);
		
		// add Hues Selection Listeners
		huesCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event)->{
			if(colorsTransition!=null) colorsTransition.stop();
			double dx = event.getX() - centerX;
			double dy = event.getY() - centerY;
			double theta = Math.atan2(dy, dx);
			double x = centerX + huesRadius * Math.cos(theta);
			double y = centerY + huesRadius * Math.sin(theta);
			colorSelector.setRotate(90+Math.toDegrees(Math.atan2(dy, dx)));
			colorSelector.setTranslateX(x - colorSelector.getPrefWidth()/2);
			colorSelector.setTranslateY(y - colorSelector.getPrefHeight()/2);
		});
		huesCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event)->{
			double dx = event.getX() - centerX;
			double dy = event.getY() - centerY;
			double theta = Math.atan2(dy, dx);
			double x = centerX + huesRadius * Math.cos(theta);
			double y = centerY + huesRadius * Math.sin(theta);
			colorsTransition = new CurveTransition(new Point2D(colorSelector.getTranslateX() + colorSelector.getPrefWidth()/2, colorSelector.getTranslateY() + colorSelector.getPrefHeight()/2), new Point2D(x,y));
			colorsTransition.play();
		});
		colorSelector.translateXProperty().addListener((o,oldVal,newVal)-> updateHSLCircleColor((int) (newVal.intValue() + colorSelector.getPrefWidth()/2), (int) (colorSelector.getTranslateY() + colorSelector.getPrefHeight()/2)));
		colorSelector.translateYProperty().addListener((o,oldVal,newVal)-> updateHSLCircleColor((int) (colorSelector.getTranslateX() + colorSelector.getPrefWidth()/2), (int) (newVal.intValue() + colorSelector.getPrefHeight()/2)));


		// Create SL Circle
		slCircleView = new ImageView(getSLCricle(pickerSize,pickerSize,new ArrayList<Stop>()));
		slCircleView.setClip(new Circle(centerX,centerY,  slRadius - 2));
		slCircleView.setPickOnBounds(false);
		this.getChildren().add(slCircleView);			

		// create SL Circle Selector
		selector = new Pane();
		Circle c1 = new Circle(selectorSize/2);
		Circle c2 = new Circle((selectorSize/2) * 0.5);
		selector.setShape(Path.subtract(c1, c2));
		selector.setStyle("-fx-border-color:#424242; -fx-border-width:1px;-fx-background-color:rgba(255, 255, 255, 0.87);");
		selector.setPrefSize(selectorSize, selectorSize);
		selector.setMinSize(selectorSize, selectorSize);
		selector.setMaxSize(selectorSize, selectorSize);
//		JFXDepthManager.setDepth(selector, 1);
		selector.setCache(true);
		selector.setMouseTransparent(true);
		this.getChildren().add(selector);

		
		// add SL selection Listeners
		slCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event)->{
			if(selectorTransition!=null) selectorTransition.stop();			
			if(Math.pow(event.getX()-centerX, 2) + Math.pow( event.getY()-centerY, 2) < Math.pow(slRadius-2, 2)){
				selector.setTranslateX(event.getX()-selector.getPrefWidth()/2);
				selector.setTranslateY(event.getY()-selector.getPrefHeight()/2);
			}else{
				double dx = event.getX() - centerX;
				double dy = event.getY() - centerY;
				double theta = Math.atan2(dy, dx);
				double x = centerX + (slRadius-2) * Math.cos(theta);
				double y = centerY + (slRadius-2) * Math.sin(theta);
				selector.setTranslateX(x-selector.getPrefWidth()/2);
				selector.setTranslateY(y-selector.getPrefHeight()/2);
			}			
		});
		slCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event)->{
			selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000), 
					new KeyValue(selector.translateXProperty(), event.getX()-selector.getPrefWidth()/2, Interpolator.EASE_BOTH),
					new KeyValue(selector.translateYProperty(), event.getY()-selector.getPrefHeight()/2, Interpolator.EASE_BOTH)))){{
						setCycleDuration(Duration.millis(160));
						setDelay(Duration.seconds(0));
					}};
					selectorTransition.play();
		});
		// add slCircleView listener
		selector.translateXProperty().addListener((o,oldVal,newVal)-> setColorAtLocation((int)newVal.intValue()+selectorSize/2, (int)selector.getTranslateY()+selectorSize/2));
		selector.translateYProperty().addListener((o,oldVal,newVal)-> setColorAtLocation((int)selector.getTranslateX()+selectorSize/2, (int)newVal.intValue()+selectorSize/2));

		
		
		// initial color selection
		double dx = 20 - centerX;
		double dy = 20 - centerY;
		double theta = Math.atan2(dy, dx);
		double x = centerX + huesRadius * Math.cos(theta);
		double y = centerY + huesRadius * Math.sin(theta);
		colorSelector.setRotate(90+Math.toDegrees(Math.atan2(dy, dx)));
		colorSelector.setTranslateX(x-colorSelector.getPrefWidth()/2);
		colorSelector.setTranslateY(y-colorSelector.getPrefHeight()/2);
		selector.setTranslateX(centerX-selector.getPrefWidth()/2);
		selector.setTranslateY(centerY-selector.getPrefHeight()/2);

	}

	
	/**
	 * List of Color Nodes that needs to be updated when picking a color
	 */
	private ObservableList<Node> colorNodes = FXCollections.observableArrayList();
	
	public void addColorSelectionNode(Node... nodes){
		colorNodes.addAll(nodes);
	}
	public void removeColorSelectionNode(Node... nodes){
		colorNodes.removeAll(nodes);
	}

	private void updateHSLCircleColor(int x, int y) {
		// transform color to HSL space
		Color color = huesCircleView.getImage().getPixelReader().getColor(x, y);
		double max = Math.max(color.getRed(), Math.max(color.getGreen(), color.getBlue())), min = Math.min(color.getRed(), Math.min(color.getGreen(), color.getBlue()));
		double hue = 0;
		if(max != min){
			double d = max - min;
			if(max == color.getRed()){
				hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
			}else if(max == color.getGreen()){
				hue = (color.getBlue() - color.getRed()) / d + 2;
			}else if(max == color.getBlue()){
				hue = (color.getRed() - color.getGreen()) / d + 4;
			}
			hue /= 6;
		}			
		currentHue = map(hue, 0,1, 0,255);

		// refresh the HSL circle
		refreshHSLCircle();
	}

	private void refreshHSLCircle() {
		ColorAdjust colorAdjust = new ColorAdjust();
		colorAdjust.setHue(map(currentHue + (currentHue<127.5? 1:-1)*127.5, 0,255, -1 , 1));
		slCircleView.setEffect(colorAdjust);
		setColorAtLocation((int)selector.getTranslateX()+selectorSize/2, (int)selector.getTranslateY()+selectorSize/2);
	}


	/**
	 * this method is used to move selectors to a certain color
	 */
	private boolean allowColorChange = true;
	private ParallelTransition pTrans;

	public void moveToColor(Color color){	
		allowColorChange = false;
		double max = Math.max(color.getRed(), Math.max(color.getGreen(), color.getBlue())), min = Math.min(color.getRed(), Math.min(color.getGreen(), color.getBlue()));
		double hue = 0;
		double l = (max + min) / 2;
		double s = 0;
		if(max == min){   
			hue = s = 0; // achromatic
		} else{
			double d = max - min;
			s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
			if(max == color.getRed()){
				hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
			}else if(max == color.getGreen()){
				hue = (color.getBlue() - color.getRed()) / d + 2;
			}else if(max == color.getBlue()){
				hue = (color.getRed() - color.getGreen()) / d + 4;
			}
			hue /= 6;
		}
		currentHue = map(hue, 0,1, 0,255);
		
		// Animate Hue
		double theta = map(currentHue, 0,255, -Math.PI, Math.PI);
		double x = centerX + huesRadius * Math.cos(theta);
		double y = centerY + huesRadius * Math.sin(theta);
		colorsTransition = new CurveTransition(new Point2D(colorSelector.getTranslateX() + colorSelector.getPrefWidth()/2, colorSelector.getTranslateY() + colorSelector.getPrefHeight()/2), new Point2D(x , y ));
		

		// Animate SL
		s = map(s, 0,1, 0,255);
		l = map(l, 0,1, 0,255);
		Point2D point = getPointFromSL((int)s, (int)l, slRadius);
		double pX = centerX - point.getX();
		double pY = centerY - point.getY();

		double endPointX;
		double endPointY;
		if(Math.pow(pX-centerX, 2) + Math.pow(pY-centerY, 2) < Math.pow(slRadius-2,2)){
			endPointX = pX-selector.getPrefWidth()/2 ;
			endPointY = pY-selector.getPrefHeight()/2;
		}else{
			double dx = pX - centerX;
			double dy = pY - centerY;
			theta = Math.atan2(dy, dx);
			x = centerX + (slRadius-2) * Math.cos(theta);
			y = centerY + (slRadius-2) * Math.sin(theta);
			endPointX = x-selector.getPrefWidth()/2 ;
			endPointY = y-selector.getPrefHeight()/2;
		}
		selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000), 
				new KeyValue(selector.translateXProperty(), endPointX, Interpolator.EASE_BOTH),
				new KeyValue(selector.translateYProperty(), endPointY, Interpolator.EASE_BOTH)))){{
					setCycleDuration(Duration.millis(160));
					setDelay(Duration.seconds(0));
				}};
		
		if(pTrans!=null) pTrans.stop();
		pTrans = new ParallelTransition(colorsTransition, selectorTransition);
		pTrans.setOnFinished((finish)->{
			if(pTrans.getStatus().equals(Status.STOPPED))
				allowColorChange = true;
		});
		pTrans.play();

		refreshHSLCircle();
	}

	private void setColorAtLocation(int x, int y) {
		if(allowColorChange){
			Color color = getColorAtLocation(x, y);
			String colorString = "rgb("+color.getRed()*255+"," +color.getGreen()*255+ "," +color.getBlue()*255+ ");";
			colorNodes.forEach(node-> node.setStyle("-fx-background-color:" + colorString + "; -fx-fill:" + colorString+";"));
		}
	}
	
	private Color getColorAtLocation(double x, double y){
		double dy = x - centerX;
		double dx = y - centerY;
		return getColor(dx, dy);
	}

	private Image getHuesCircle(int width, int height, List<Stop> stops) {
		WritableImage raster = new WritableImage(width, height);
		PixelWriter pixelWriter = raster.getPixelWriter();
		Point2D center = new Point2D(width / 2, height / 2); 
		double rsmall = 0.8*width / 2;
		double rbig = width / 2;
		for (int y = 0 ; y < height ; y++) {
			for (int x = 0 ; x < width ; x++) {
				double dx = x - center.getX();
				double dy = y - center.getY();
				double distance = Math.sqrt((dx * dx) + (dy * dy));
				double o = Math.atan2(dy, dx);
				if(distance > rsmall && distance < rbig){
					double H = map(o,-Math.PI, Math.PI, 0 , 255);
					double S = 255;
					double L = 152;
					pixelWriter.setColor(x, y, HSL2RGB(H,S,L));
				}
			}
		}
		return raster;
	}
	
	private Image getSLCricle(int width, int height, List<Stop> stops) {
		WritableImage raster = new WritableImage(width, height);
		PixelWriter pixelWriter = raster.getPixelWriter();
		Point2D center = new Point2D(width / 2, height / 2); 
		for (int y = 0 ; y < height ; y++) {
			for (int x = 0 ; x < width ; x++) {
				double dy = x - center.getX();
				double dx = y - center.getY();
				pixelWriter.setColor(x, y, getColor(dx, dy));
			}
		}
		return raster;
	}
	
	private double clamp(double from,double small,double big) {
		return Math.min(Math.max(from,small),big);
	}
	
	private Color getColor(double dx, double dy){
		double distance = Math.sqrt((dx * dx) + (dy * dy));
		double rverysmall = 0.65*(pickerSize/2);
		Color pixelColor = Color.BLUE;

		if (distance <= rverysmall*1.1) {
			double angle = -Math.PI/2.;
			double angle1 = angle+2*Math.PI/3.;
			double angle2 = angle1+2*Math.PI/3.;
			double x1 = rverysmall*Math.sin(angle1);
			double y1 = rverysmall*Math.cos(angle1);
			double x2 = rverysmall*Math.sin(angle2);
			double y2 = rverysmall*Math.cos(angle2);
			dx += 0.01;
			double[] circle = circleFrom3Points(new Point2D(x1,y1),new Point2D(x2,y2),new Point2D(dx,dy));
			double xArc = circle[0];
			double yArc = 0;
			double arcR = circle[2];
			double Arco = Math.atan2(dx-xArc,dy-yArc);
			double Arco1 = Math.atan2(x1-xArc,y1-yArc);
			double Arco2 = Math.atan2(x2-xArc,y2-yArc);

			double finalX = xArc>0 ? xArc-arcR : xArc+arcR;
			
			double saturation = map(finalX,-rverysmall,rverysmall,255,0);

			double lightness = 255;
			double diffAngle = Arco2 - Arco1;
			double diffArco = Arco - Arco1;
			if(dx<x1) {     
				diffAngle = diffAngle<0 ? 2*Math.PI+diffAngle:diffAngle;
				diffAngle = Math.abs(2*Math.PI-diffAngle);
				diffArco = diffArco<0 ? 2*Math.PI+diffArco : diffArco;
				diffArco = Math.abs(2*Math.PI-diffArco);
			}
			lightness = map(diffArco,0,diffAngle,0,255);
			

			if(distance > rverysmall) {
				saturation = 255-saturation;
				if(lightness < 0 && dy < 0) {
					lightness = 255;
				}
			}
			lightness = clamp(lightness,0,255);
			if((saturation < 10 && dx < x1) || (saturation >240 && dx > x1)) {
				saturation = 255-saturation;
			}
			saturation = clamp(saturation,0,255);
			pixelColor = HSL2RGB(currentHue,saturation,lightness);
		}
		return pixelColor; 
	}
	

	/***************************************************************************
	 *                                                                         *
	 * Hues Animation                                                          *
	 *                                                                         *
	 **************************************************************************/
	
	private final class CurveTransition extends Transition {
		Point2D from;
		double fromTheta;
		double toTheta;
		public CurveTransition(Point2D from, Point2D to) {
			this.from = from;
			double fromDx = from.getX() - centerX;
			double fromDy = from.getY() - centerY;
			fromTheta = Math.atan2(fromDy, fromDx);
			double toDx = to.getX() - centerX;
			double toDy = to.getY() - centerY;
			toTheta = Math.atan2(toDy, toDx);
			setInterpolator(Interpolator.EASE_BOTH);
			setDelay(Duration.millis(0));
			setCycleDuration(Duration.millis(240));
		}
		@Override
		protected void interpolate(double frac) {
			double dif = Math.min(Math.abs(toTheta - fromTheta), 2*Math.PI - Math.abs(toTheta - fromTheta));
			if(dif == 2*Math.PI - Math.abs(toTheta - fromTheta)){
				int dir = -1;
				if(toTheta < fromTheta) dir = 1;
				dif = dir*dif;
			} else dif = toTheta - fromTheta;
			
			Point2D newP = rotate(from, new Point2D(centerX,centerY), frac * dif );
			colorSelector.setRotate(90+Math.toDegrees(Math.atan2(newP.getY()-centerY, newP.getX()-centerX)));
			colorSelector.setTranslateX(newP.getX() - colorSelector.getPrefWidth()/2);
			colorSelector.setTranslateY(newP.getY() - colorSelector.getPrefHeight()/2);
		}
	}

	
	
	/***************************************************************************
	 *                                                                         *
	 * Util methods	                                                           *
	 *                                                                         *
	 **************************************************************************/
	
	private double map(double val, double min1, double max1, double min2, double max2){
		return min2+(max2-min2)*((val-min1)/(max1-min1));
	}

	private Color HSL2RGB(double hue, double sat, double lum)
	{
		hue = map(hue,0,255,0,359);
		sat = map(sat,0,255,0,1);
		lum = map(lum,0,255,0,1);
		double v;
		double red, green, blue;
		double m;
		double sv;
		int sextant;
		double fract, vsf, mid1, mid2;

		red = lum;   // default to gray
		green = lum;
		blue = lum;
		v = (lum <= 0.5) ? (lum * (1.0 + sat)) : (lum + sat - lum * sat);
		m = lum + lum - v;
		sv = (v - m) / v;
		hue /= 60.0;  //get into range 0..6
		sextant = (int) Math.floor(hue);  // int32 rounds up or down.
		fract = hue - sextant;
		vsf = v * sv * fract;
		mid1 = m + vsf;
		mid2 = v - vsf;

		if (v > 0)
		{
			switch (sextant)
			{
			case 0: red = v; green = mid1; blue = m; break;
			case 1: red = mid2; green = v; blue = m; break;
			case 2: red = m; green = v; blue = mid1; break;
			case 3: red = m; green = mid2; blue = v; break;
			case 4: red = mid1; green = m; blue = v; break;
			case 5: red = v; green = m; blue = mid2; break;
			}
		}
		return new Color(red, green, blue, 1);
	}
	
	private double[] circleFrom3Points(Point2D a,Point2D b, Point2D c) {
		double ax,ay,bx,by,cx,cy,x1,y11,dx1,dy1,x2,y2,dx2,dy2,ox,oy,dx,dy,radius; // Variables Used and to Declared
		ax =a.getX() ; ay = a.getY(); //first Point X and Y
		bx =b.getX(); by = b.getY(); // Second Point X and Y
		cx =c.getX() ; cy =c.getY(); // Third Point X and Y

		////****************Following are Basic Procedure**********************///
		x1 = (bx + ax) / 2;
		y11 = (by + ay) / 2;
		dy1 = bx - ax;
		dx1 = -(by - ay);

		x2 = (cx + bx) / 2;
		y2 = (cy + by) / 2;
		dy2 = cx - bx;
		dx2 = -(cy - by);

		ox = (y11 * dx1 * dx2 + x2 * dx1 * dy2 - x1 * dy1 * dx2 - y2 * dx1 * dx2)/ (dx1 * dy2 - dy1 * dx2);
		oy = (ox - x1) * dy1 / dx1 + y11;

		dx = ox - ax;
		dy = oy - ay;
		radius =Math.sqrt(dx * dx + dy * dy);	
		double[] circle = {ox,oy,radius};
		return circle;
	}

	
	private Point2D getPointFromSL(int saturation, int lightness, double radius) {
		double dy = map(saturation,0,255,-radius,radius);
		double angle = 0.;
		double angle1 = angle+2*Math.PI/3.;
		double angle2 = angle1+2*Math.PI/3.;
		double x1 = radius*Math.sin(angle1);
		double y1 = radius*Math.cos(angle1);
		double x2 = radius*Math.sin(angle2);
		double y2 = radius*Math.cos(angle2);
		double dx = 0;
		double[] circle = circleFrom3Points(new Point2D(x1,y1),new Point2D(dx,dy),new Point2D(x2,y2));
		double xArc = circle[0];
		double yArc = circle[1];
		double arcR = circle[2];
		double Arco1 = Math.atan2(x1-xArc,y1-yArc);
		double Arco2 = Math.atan2(x2-xArc,y2-yArc);
		double ArcoFinal = map(lightness,0,255,Arco2,Arco1);
		double finalX = xArc + arcR*Math.sin(ArcoFinal);
		double finalY = yArc + arcR*Math.cos(ArcoFinal);
		if(dy<y1) {
			ArcoFinal = map(lightness,0,255,Arco1,Arco2+2*Math.PI);
			finalX = -xArc - arcR*Math.sin(ArcoFinal);
			finalY = yArc + arcR*Math.cos(ArcoFinal);
		}
		return new Point2D(finalX,finalY);
	}
	
	private Point2D rotate(Point2D a, Point2D center, double angle){
		double resultX = center.getX() + (a.getX() - center.getX())*Math.cos(angle) -  (a.getY() - center.getY())*Math.sin(angle);
		double resultY = center.getY() + (a.getX() - center.getX())*Math.sin(angle) +  (a.getY() - center.getY())*Math.cos(angle);
		return new Point2D(resultX,resultY);
	}

}